/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is mozilla.org code.
 *
 * The Initial Developer of the Original Code is Robert Morris.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * ***** END LICENSE BLOCK ***** */

package com.pavisoft.irecipe;

/**
 *
 * @author Robert Morris
 *
 * Designed to hold whole or fractional values that represent a quantity,
 * such as "3 1/2".
 *
 */
public class Quantity implements Comparable<Quantity>, Cloneable
{
    private int wholeNumber = 0;
    private int fractionalNumerator = 0;
    private int fractionalDenominator = 0;
   
    /**
     * Creates a new <code>Quantity</code> equal to the given integer.
     */
    public Quantity(int number)
    {
        this.wholeNumber = number;
    }

    /**
     * Creates a new <code>Quantity</code> similar to a mixed number with the
     *  given whole part plus the given fractional parts
     */
    public Quantity(int wholeNumber, int fractionalNumerator, int FractionalDenominator)
    {
        this.wholeNumber = wholeNumber;
        this.fractionalNumerator = fractionalNumerator;
        this.fractionalDenominator = FractionalDenominator;
    }
    
    /**
     * Tries to parse a string and return a <code>Quantity</code> that represents
     * the value denoted by the string. (It is recommended that code using the
     * return value of this function check it against <code>null</code>; see
     * notes below.)
     * @param stringToParse the string to parse for a value.
     * @returns A <code>Quantity</code> representing the value denoted by the
     *  string <code>stringToParse</code>, or <code>null</code> if could
     *  not parse.
     */
    public static Quantity parseQuantity(String stringToParse)
    {
        int whole = 0;
        int numer = 0;
        int denom = 0;
        boolean isValid = false;
        
        if (stringToParse.indexOf("/") == -1)
        {
            // no slash character found, so probably whole number
            try
            { 
                Integer w = new Integer(stringToParse);
                whole = w;
                isValid = true;
            }
            catch (NumberFormatException ex)
            {}
        }
        else
        {
            int locSpace = stringToParse.indexOf(" ");
            int locSlash = stringToParse.indexOf("/");
            String strWhole = null;
            
            if (locSpace != -1)
            {
                strWhole = stringToParse.substring(0, locSpace);
            }
            int start = locSpace + 1;
            
            String strNumer = stringToParse.substring(start, locSlash);
            String strDenom = stringToParse.substring(locSlash + 1);
            if (!(locSpace == -1 || locSlash == -1))
            {
                try
                {
                    Integer w = new Integer(strWhole);
                    Integer n = new Integer(strNumer);
                    Integer d = new Integer(strDenom);
                    isValid = true;
                    whole = w;
                    numer = n;
                    denom = d;
                }
                catch (NumberFormatException ex)
                {}
            }
            else if (locSlash != -1)
            {
                //probably fraction without whole number
                Integer w = 0;
                try
                {
                    Integer n = new Integer(strNumer);
                    Integer d = new Integer(strDenom);
                    isValid = true;
                    whole = w;
                    numer = n;
                    denom = d;
                }
                catch (NumberFormatException ex)
                {}
            }
        }
        if (!isValid)
        {
            return null;
        }
        return new Quantity(whole, numer, denom);
    }
    
    /**
     * Adds the given integer to this Quantity
     */
    public void add(int intToAdd)
    {
        this.wholeNumber += intToAdd;
    }
    
    /**
     * Adds together the given <code>Quantity</code>s and returns a new
     * <code>Quantity</code> representing the result.
     */
    public static Quantity add(Quantity quantity1, Quantity quantity2)
    {       
        Quantity newQ;

        Quantity[] q = Quantity.convertFractionsToLike(quantity1, quantity2);
        
        int newNumer = q[0].getFractionalNumerator() + q[1].getFractionalNumerator();
        int newDenom = q[0].getFractionalDenominator();
        int newWhole = quantity1.getWholeNumberComponent() + quantity2.getWholeNumberComponent();
        
        newQ = new Quantity(newWhole, newNumer, newDenom);
        newQ.reduce();       
        return newQ;
    }
    
    /**
     * Changes fractional component of the two given <code>Quantity</code> instances
     * to both have same denominator; useful for facilitating comparisons.
     * @return An array containing two elements, the first of which has the same
     *  value as <code>firstQuantity</code>, and the second of which has the same
     *  value as <code>secondQuantity</code>, such that both returned
     *  <code>Quantity</code> instances have the same fractional denominator.
     */
    private static Quantity[] convertFractionsToLike(Quantity firstQuantity, Quantity secondQuantity)
    {
        if (firstQuantity.getFractionalDenominator() == 0)
        {
            firstQuantity.setFractionalComponent(0,1);
        }
        if (secondQuantity.getFractionalDenominator() == 0)
        {
            secondQuantity.setFractionalComponent(0,1);
        }
        int newDenom = secondQuantity.getFractionalDenominator() * firstQuantity.getFractionalDenominator();
        int newNumerThis = firstQuantity.getFractionalNumerator() * secondQuantity.getFractionalDenominator();
        int newNumerOther = secondQuantity.getFractionalNumerator() * firstQuantity.getFractionalDenominator();
        firstQuantity.setFractionalComponent(newNumerThis, newDenom);
        secondQuantity.setFractionalComponent(newNumerOther, newDenom);
        return new Quantity[] {firstQuantity, secondQuantity};
    }
   
    /**
     * Returns whole number component of this <code>Quantity</code>; e.g., if the <code>Quantity</code> is
     *  equal to 5 2/3, this method returns 5.
     */
    public int getWholeNumberComponent()
    {
        return this.wholeNumber;
    }
    
    /**
     * Returns the numerator of the fractional component of this <code>Quantity</code>; e.g.,
     * if the <code>Quantity</code> is equal to 5 2/3, this method returns 2.
     */    
    public int getFractionalNumerator()
    {
        return this.fractionalNumerator;
    }
    
    /**
     * Returns the denominator of the fractional component of this <code>Quantity</code>; e.g.,
     * if the <code>Quantity</code> is equal to 5 2/3, this method returns 3.
     */
    public int getFractionalDenominator()
    {
        return this.fractionalDenominator;
    }
    
    /**
     * Sets fractional component of this <code>Quantity</code> to the given numerator
     * and denominator.
     */
    public void setFractionalComponent(int numerator, int denominator)
    {
        this.fractionalNumerator = numerator;
        this.fractionalDenominator = denominator;
    }
    
    /** Returns greatest common factor between the two given numbers. */
    private static int gcf(int num1, int num2)
    {
        if(num2 <= num1 && num1 % num2 == 0)
            return num2;
        else if(num1 < num2)
            return gcf(num2, num1);
        else
            return gcf(num2, num1 % num2);
    }
    
    /**
     * Reduces the fractional component, if any, of this <code>Quantity</code>, eliminating
     *  improper fractions if necessary. E.g., 5 1/1 becomes 6; 5 2/4 becomes
     *  5 1/2; and 5 2/1 becomes 6 1/2.
     */    
    public void reduce()
    {
        if (this.fractionalDenominator == 0) return;
        if (this.fractionalNumerator == 0)
        {
            this.setFractionalComponent(0,0);
            return;
        }
        int factor = gcf(fractionalNumerator, fractionalDenominator);
        fractionalNumerator /= factor;
        fractionalDenominator /= factor;
        while (fractionalNumerator > fractionalDenominator)
        {
            int diff = fractionalNumerator - fractionalDenominator;
            this.fractionalNumerator -= this.fractionalDenominator;
            this.wholeNumber += 1;
        }
        if (fractionalNumerator == fractionalDenominator)
        {
            this.wholeNumber += 1;
            this.setFractionalComponent(0,0);
        }
    }
    
    /** Returns a human-friendly representation of this <code>Quantity</code>,
     *  such as "1" or "2 1/3".
     */
    public String toString()
    {
        String num = Long.toString(this.wholeNumber);
        if (fractionalDenominator != 0 && fractionalNumerator != 0)
        {
            num += " ";
            if (num.equals("0 "))
            {
                num = "";
            }
            num += fractionalNumerator + "/" + fractionalDenominator;
        }
        return num;
    }
    
    /** Returns a <code>double</code> representing the value of this <code>Quantity</code> */
    public double toDouble()
    {
        double whole = this.wholeNumber;
        double fract = 0;
        if (this.fractionalDenominator != 0)
            fract = this.fractionalNumerator / this.fractionalDenominator;
        return whole + fract;
    }
    
    /**
     * Returns a new <code>Object</code> (that is an instance of <code>Quantity</code>) <code>q2</code> representing the same value as
     * this <code>Quantity</code> <code>q1</code> such that <code>q2.equals(q1) == true</code>.
     */    
    public Object clone()
    {
        Quantity q = new Quantity(this.wholeNumber, this.fractionalNumerator, 
                this.fractionalDenominator);
        return q;
    }
  
    public int compareTo(Quantity q)
    {
        Quantity a = (Quantity)this.clone();
        Quantity b = (Quantity)q.clone();
        a.reduce();
        b.reduce();
        int retval = 0;
        if (a.getWholeNumberComponent() < b.getWholeNumberComponent())
        {
            retval = -1;
        }
        else if (a.getWholeNumberComponent() == b.getWholeNumberComponent())
        {
            Quantity[] qs = Quantity.convertFractionsToLike(a, b);
            a = qs[0];
            b = qs[1];
            if (a.getFractionalNumerator() < b.getFractionalNumerator())
            {
                retval = -1;
            }
            else if (a.getFractionalNumerator() == b.getFractionalNumerator())
            {
                retval = 0;
            }
            else if (a.getFractionalNumerator() > b.getFractionalNumerator())
            {
                retval = 1;
            }
        }
        else if (a.getWholeNumberComponent() > b.getWholeNumberComponent())
        {
            retval = 1;
        }
        return retval;
    }
    
    public boolean equals(Object object)
    {
        Quantity other = null;
        if (!(object instanceof Quantity))
        {
            if (!(object instanceof Double))
            {
                if (object instanceof Integer)
                {
                    other = new Quantity(new Integer(object.toString()));
                }
                else
                {
                    // Not another instance of this class, nor a numerical value
                    // easily converted, so return false.                    
                    return false;
                }
            }
            else
            {
                Double d = new Double(object.toString());
                return (d.doubleValue() == this.toDouble());
            }
        }
        if (other == null)
            other = (Quantity)object;
        return (other.compareTo(this) == 0);
    }
}
